using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using SpoonCore2.Client;
using SpoonCore2.Message;
using SpoonCore2.OverlayNetwork.Pastry.Messages;
using System.Net;
using SpoonCore2.Routing;
using SpoonCore2.Service;
using SpoonCore2.Tools;
using Timer = System.Timers.Timer;

namespace SpoonCore2.OverlayNetwork.Pastry
{
	public class PastryNetwork : AbstractService, IOverlayNetwork
	{
		private const int ExchangeInterval = 1 * 1000;
		private const int PingInterval = 10 * 1000;
		private static readonly TimeSpan PingTimeout = new TimeSpan(0, 0, 10);

		private static readonly SpoonIdentifier _id = SpoonIdentifier.Parse("52E7F0F6DF97AEACFD1781586D6E3454");
		private static readonly Random _rand = new Random();

		private readonly ClientInfo _info;
		private readonly List<ITable> _tables = new List<ITable>();
		private readonly RoutingTable _routingTable;
		private readonly LeaveTable _leaveTable;
		private readonly UpdateTable _updateTable;
		private readonly Timer _exchangeTimer;
		private readonly Timer _pingTimer;
		
		private readonly List<IPEndPoint> _bootstraps = new List<IPEndPoint>();

		private readonly SortedList<int, PingItem> _pingList = new SortedList<int, PingItem>();

		public PastryNetwork()
		{
			_info = new ClientInfo(this, new SpoonIdentifier());

			_leaveTable = new LeaveTable(this);
			_tables.Add(_leaveTable);
			_routingTable = new RoutingTable(this);
			_tables.Add(_routingTable);
			_updateTable = new UpdateTable();
			_tables.Add(_updateTable);

			Handler.Register(new MessageHandler.ReceiveMessageHandler<PingMessage>(PingReceive));
			Handler.Register(new MessageHandler.ReceiveMessageHandler<JoinMessage>(JoinReceive));
			Handler.Register(new MessageHandler.ReceiveMessageHandler<ExchangeMessage>(ExchangeReceive));

			_exchangeTimer = new Timer(ExchangeInterval);
			_exchangeTimer.Elapsed += _exchangeTimer_Elapsed;
			_exchangeTimer.Start();

			_pingTimer = new Timer(PingInterval);
			_pingTimer.Elapsed += _pingTimer_Elapsed;
			//_pingTimer.Start();
		}

		private void _pingTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
		{
			Monitor.Enter(_pingList);
			// create new ping for all leave nodes
			foreach (ClientInfo client in _leaveTable.Clients)
			{
				PingMessage msg = new PingMessage();
				_pingList.Add(msg.Id, new PingItem(msg, client));
				client.Send(msg);
			}

			// remove TimeoutMessage
			for (int x = 0; x < _pingList.Values.Count; x++)
			{
				if (DateTime.Now - _pingList.Values[x].Message.Time > PingTimeout)
				{
					ColorConsole.WriteLine("Timeout for " + _pingList.Values[x].Client.Id, ConsoleColor.Red);
					RemoveClient(_pingList.Values[x].Client);
					_pingList.RemoveAt(x);
					x--;
				}
			}
			Monitor.Exit(_pingList);
		}

		private class PingItem
		{
			public readonly PingMessage Message;
			public readonly ClientInfo Client;

			public PingItem(PingMessage msg, ClientInfo client)
			{
				Message = msg;
				Client = client;
			}
		}

		private void _exchangeTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
		{
			_routingTable.Refresh();
		}

		private void Join()
		{
			if (_bootstraps.Count > 0)
			{
				IPEndPoint endpoint = _bootstraps[_rand.Next(_bootstraps.Count - 1)];
				Send(endpoint, new JoinMessage(new ClientInfo(this, _info.Id)));
			}
		}

		public ClientInfo ClientInfo
		{
			get { return _info; }
		}

		public override SpoonIdentifier Id
		{
			get { return _id; }
		}

		private void RemoveClient(ClientInfo client)
		{
			_leaveTable.Remove(client);
			_routingTable.Remove(client);
		}

		private void ExchangeReceive(ClientInfo sender, ExchangeMessage msg)
		{
			if (msg.Type == ExchangeMessage.ExchangeType.Request)
			{
				ExchangeMessage res = new ExchangeMessage();
				res.Type = ExchangeMessage.ExchangeType.Response;
				res.Clients.AddRange(_routingTable.FindByMask(msg.SearchList));
				if (msg.LeaveSet) res.Clients.AddRange(_leaveTable.Clients);

				if (res.Clients.Count > 0)
				{
					ColorConsole.WriteLine(_info.Id + ": Send new info to " + sender.Id + ": " + res.Clients.Count);
					sender.Send(res);
				}

				_updateTable.Register(sender, msg.SearchList);
			}

			if (msg.Type == ExchangeMessage.ExchangeType.Response)
			{
				ColorConsole.WriteLine(_id + ": Receive " + msg.Clients.Count + " infos from " + sender.Id);
				AddClient(msg.Clients);
			}
		}

		public ClientInfo GetClientInfo(SpoonIdentifier id)
		{
			List<ClientInfo> resList = new List<ClientInfo>();
			resList.Add(ClientInfo);
			foreach (ITable table in _tables)
			{
				ClientInfo tmp = table.FindNext(id);
				if (tmp != null && !resList.Contains(tmp)) resList.Add(tmp);
			}

			ClientInfo res = null;
			foreach (ClientInfo info in resList)
			{
				if (res == null) res = info;
				else res = (id.Nearest(res.Id, info.Id, SpoonIdentifier.Direction.Both) == res.Id ? res : info);
			}
			return res;
		}

		private void JoinReceive(ClientInfo destClient, JoinMessage msg)
		{
			if (msg.Type == JoinMessage.JoinType.Request)
			{
				if (msg.JoinInfo.Id == destClient.Id) msg.JoinInfo.AddEndpoint(destClient.Endpoints); // Add source endpoints

				ClientInfo next = _routingTable.FindNext(msg.JoinInfo.Id); //TODO why not GetClientInfo???
				if (next != null)
				{
					// Forward
					ColorConsole.WriteLine(ClientInfo.Id + ": Forward JoinMessage");
					next.Send(msg);
				}

				// Add to local routing table
				AddClient(msg.JoinInfo);
				AddClient(destClient);

				// Send local routing table
				JoinMessage res = new JoinMessage();
				res.JoinInfo = msg.JoinInfo;
				res.Type = JoinMessage.JoinType.Response;
				res.Clients.AddRange(_routingTable.NodesFor(msg.JoinInfo));
				if (msg.JoinInfo.Id != ClientInfo.Id) msg.JoinInfo.Send(res);
				//ColorConsole.WriteLine(_client.Id + ": Send local routing table (" + res.Clients.Count + ")");
			}
			if (msg.Type == JoinMessage.JoinType.Response)
			{
				AddClient(msg.Clients);
				ColorConsole.WriteLine(ClientInfo + ": JoinMessage response from " + destClient.Id + "(" + msg.Clients.Count + ")");
			}
		}

		public void AddClient(IEnumerable<ClientInfo> clients)
		{
			foreach (ClientInfo info in clients)
			{
				AddClient(info);
			}
		}

		public void AddClient(ClientInfo info)
		{
			foreach (ITable table in _tables)
			{
				table.Add(info);
			}
		}

		public void AddBootstrap(IPEndPoint point)
		{
			_bootstraps.Add(point);
		}

		public bool Check(List<ClientInfo> clients)
		{
			bool status = true;
			if (!_routingTable.Check(clients)) status = false;
			if (!_leaveTable.Check(clients)) status = false;
			return status;
		}

		public override void Start()
		{
			Join();
		}

		public bool Successor(SpoonIdentifier id)
		{
			return GetClientInfo(id).Id == ClientInfo.Id;
		}

		private void PingReceive(ClientInfo destClient, PingMessage msg)
		{
			if (msg.Type == PingMessage.MessageType.Request)
			{
				PingMessage res = new PingMessage();
				res.Type = PingMessage.MessageType.Response;
				res.Id = msg.Id;
				destClient.Send(res);
			}
			if (msg.Type == PingMessage.MessageType.Response)
			{
				Monitor.Enter(_pingList);
				if (_pingList.ContainsKey(msg.Id)) _pingList.Remove(msg.Id);
				Monitor.Exit(_pingList);
			}
		}

		public override void Dispose()
		{

		}

		public override void Init()
		{

		}

		public override void Stop()
		{
			
		}
	}
}